#!/usr/bin/python

###################################################################################
#   Copyright 2010 Jason Hoover
#
#
#   This file is part of AutoCache.
#
#   AutoCache is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   AutoCache is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU Lesser General Public License
#   along with AutoCache.  If not, see <http://www.gnu.org/licenses/>.
###################################################################################

##########################
# ac-common - Contains functions every program will need, including report generators or the like.
# 
# Function Name		Description				Usage			Returns
# ac_configurure	Configuration parser			ac_configure(conffile)	DICT of Settings				
# ac_debug		Debug logger. 				ac_debug(message)	Nothing
# ac_info 		Info Logger.  				ac_info(text)		Nothing
# ac_df			Generates disk free percentage.		ac_df(path)		INT Percentage of disk free
# ac_du			Lambda Runs du for specified path	ac_du(path)		INT Disk usage of path in MB
# ac_urltohash		Lambda converts a URLs to hash		ac_url(str url)		STR MD5 Hash of second part.
# ac_urltofle		Converts URLs into filenames		ac_urltofile(url)	A filename, derived from the URL
# ac_stateinit		Creates state directories/files.	ac_stateinit()		Nothing
########################## 

import os
import subprocess
import urlparse
import hashlib
import logging

####################
# ac_configure - Loads the configuration file into memory, parses it, and reads those settings into 
# ac_settings, which it then returns as it's result. All settings should be specified, loaded, and
# parsed here. Everything else should be considered temporary. Should typically only be executed 
# once per exectutable at runtime. All of the other applications should load this.
####################

def ac_configure ( config_location, cmdopts ):
	
	# Set up sane defaults.
	# WARNING This ac_settings is local. This function returns ac_settings which must be set globally!
	
	ac_settings = {
		"log_file"		:	False,
		"log_level"		:	0,
		"log_format"		:	"%(asctime)s [%(process)d:%(threadName)s] %(levelname)s %(funcName)s: %(message)s",
		"du_exec"		:	"/usr/bin/du",	
		"dl_exec"		:	"/usr/bin/wget",
		"dl_bwlimit" 		:	0,
		"dl_slots"		:	1,
		"nice"			:	0,
		"nice_exec"		:	"/bin/nice",
		"cache_dir"		:	"",
		"cache_web"		: 	"",
		"cache_pctlimit"	:	90,
		"cache_mblimit"		:	0,
		"trust_long"		:	1000,
		"trust_short"		:	60,
		"state_dir"		:	"",
		"targets"		:	"",
		"concurrency"		:	False,
		"logger"		:	False,
		}

	# Define words that imply true or false so they can be used in the config if necessary.

	true_false = {
		"true"	: True,
		"1"	: True,
		"yes"	: True,
		"0"	: False,
		"no"	: False,
		"false"	: False,
		}

	# Open and read the configuration file object. Die if that fails.
	
	try:
		config_file = open( config_location )

		# Read the file into a dict. Read line by line, strip newlines, make it lower case and split at ='s.

		config_contents = {}
		lastline = config_file.readline().rstrip().lower().split('=')	
	
		while lastline[0] != "":			
			config_contents[lastline[0]] = lastline[1]
			lastline = config_file.readline().rstrip().lower().split('=')
		del lastline

	except IOError:
		raise IOError ("FAILURE: Configuration not specified or unreadable.")
		

	# Create the logger object regardless of if it's configured. We'll be using it no matter
	# if it's configured or not!

	ac_settings["logger"] = logging.getLogger("ac_log")

	if config_contents.has_key("log_file") and config_contents["log_file"] > "":

		log_handler=logging.FileHandler(config_contents["log_file"])

		# Set up the formatting. Has to be done before the handler is added to the logger.

		if config_contents.has_key("log_format") and config_contents["log_format"] > "":
			log_handler.setFormatter(logging.Formatter(config_contents["log_format"]))
		else:
			log_handler.setFormatter(logging.Formatter(ac_settings["log_format"]))


		# Add a log file handler.

		ac_settings["logger"].addHandler(log_handler)


	# Set up the log level.

	if config_contents.has_key("log_level") and config_contents["log_level"] > "":

		# Define the levels here.

		log_levels = {	'debug'		: logging.DEBUG,
				'info'		: logging.INFO,
				'warning'	: logging.WARNING,
				'error'		: logging.ERROR,
				'critical'	: logging.CRITICAL
			}

		# And then set the correct level.

		ac_settings["logger"].setLevel(log_levels.get(config_contents["log_level"], logging.NOTSET))

	# Set wget exec variable if set. 

	if config_contents.has_key("dl_exec") and os.access(config_contents["dl_exec"],os.X_OK):
			ac_settings["dl_exec"]=config_contents["dl_exec"]
	else:
		try:
			subprocess.call("wget",stdout=subprocess.PIPE)
			ac_settings["dl_exec"]="wget"
		except OSError:
			raise OSError ("FAILURE: Cannot find wget in PATH. Buhhhh?!")


	# Same for nice. Put stderr into a pipe, because nice complains on some systems.

	if config_contents.has_key("nice_exec") and os.access(config_contents["nice_exec"],os.X_OK):
		ac_settings["nice_exec"]=config_contents["nice_exec"]
	else:
		try:
			subprocess.call("nice",stdout=subprocess.PIPE,stderr=subprocess.PIPE)
			ac_settings["nice_exec"]="nice"
		except OSError:
			raise OSError("FAILURE: Cannot find nice in PATH. Buhhhh?!")

	# Same for du. (Maybe I should make a function for this.. >___>)

	if config_contents.has_key("du_exec") and os.access(config_contents["du_exec"],os.X_OK):
			ac_settings["du_exec"]=config_contents["du_exec"]
	else:
		try:
			subprocess.call(["du","/dev/null"],stdout=subprocess.PIPE)
			ac_settings["du_exec"]="du"
		except OSError:
			raise OSError("FAILURE: Cannot find du in PATH. Buhhhh?!")


	# Set the dl_bwlimit value in kilobytes. It should be higher than 0, less than 999 gigabytes per second.

	# If not set, leave the default value and do no further processing.
	# If it's an insane number (like -20), raise one ourselves.

	if not config_contents.has_key("dl_bwlimit"):
		pass
	elif int(config_contents["dl_bwlimit"]) > 0 or int(config_contents["dl_bwlimit"]) < 999999999:
		ac_settings["dl_bwlimit"]=int(config_contents["dl_bwlimit"])
	else:
		raise ValueError("FAILURE: dl_bwlimit set, but either not a number, negative, zero, or -way- too high! Setting was: "+config_contents["dl_bwlimit"])


	# Max disk usage percent.

	# If the number isn't set, leave the default and do no further processing.
	# If it's valid, set it to that. 
	# If not either, raise a value error.
	
	if not config_contents.has_key("cache_pctlimit"):
		pass
	elif int(config_contents["cache_pctlimit"]) > 0 and int(config_contents["cache_pctlimit"]) < 100:
		ac_settings["cache_pctlimit"]=int(config_contents["cache_pctlimit"])
	else:
		raise ValueError("FAILURE: cache_pctlimit set, but either not a number, negative, zero, or more than 100. Setting was: "+config_contents["cache_pctlimit"])

	# Download slots.

	if not config_contents.has_key("dl_slots"):
		pass
	elif int(config_contents["dl_slots"]) > 0 and int(config_contents["dl_slots"]) < 1000:
		ac_settings["dl_slots"]=int(config_contents["dl_slots"])
	else:
		raise ValueError("FAILURE: dl_slots set, but either not a number, negative, zero, or more than 100. Setting was: "+config_contents["dl_slots"])



	# Max disk usage in MB
	
	if config_contents.has_key("cache_mblimit") and int(config_contents["cache_mblimit"]) > 0:
		ac_settings["cache_mblimit"]=int(config_contents["cache_mblimit"])*1024*1024
	else:
		raise ValueError("FAILURE: cache_mblimit either not set, not a number, negative, or zero, Setting was: "+config_contents["cache_pctlimit"])
		

	# Set up cache directory.

	if not config_contents.has_key("cache_dir"):
		raise KeyError("FAILURE: cache_dir unset! WTF am I supposed to do without one?!")

	elif os.access(config_contents["cache_dir"],os.R_OK + os.W_OK) and os.path.isdir(config_contents["cache_dir"]):

			# Add a trailing slash now so I don't have to enter it in the code 15 fucking times.

			ac_settings["cache_dir"]=config_contents["cache_dir"]+"/"

	else:
		raise OSError("FAILURE: cache_dir either unreadable, unwritable, unlistable, or not actually a directory. Setting was: " + config_contents["cache_dir"])

	# Set up state directory. Sorry this is such a huge chunk of code, but it's important.

	if not config_contents.has_key("state_dir"):
		raise KeyError("FAILURE: state_dir unset! Needed for most normal operations. Cannot continue.")

	if os.access(config_contents["state_dir"],os.R_OK + os.W_OK) and os.path.isdir(config_contents["state_dir"]):

		# Add a trailing slash now so I don't have to enter it in the code 15 fucking times.

		ac_settings["state_dir"]=config_contents["state_dir"]+"/"

	else:
		raise OSError("FAILURE: state_dir either unreadable, unwritable, unlistable, or not actually a directory. Setting was: " + config_contents["state_dir"])



	# Validate the existance of the targets regex list, but don't bother parsing it.

	try:
		if os.access(config_contents["targets"],os.R_OK) and os.path.isfile(config_contents["targets"]):
			ac_settings["targets"]=config_contents["targets"]

	except IOError:
		raise IOError("FAILURE: Could not open targets file specified in "+config_location)

	except KeyError:
		raise KeyError("FAILURE: No targets file specified.")

	# Set up URL base

	try:
		if urlparse.urlparse(config_contents["cache_web"]).scheme != "http":
			raise ValueError("FAILURE: Only http destination URLs presently supported. Setting was: " + config_contents["cache_web"])
			
		elif urlparse.urlparse(config_contents["cache_web"]).netloc == "":
			raise ValueError("FAILURE: Either target URL badly written, or you need at least a hostname...")

		else:
			ac_settings["cache_web"]=config_contents["cache_web"]
	except KeyError:
		raise KeyError("FAILURE: Destination URL unset.")

	
	# Lastly, validate nice, leave default if unset.
	# From nice(1), should be between -20 and 19.

	if not config_contents.has_key("nice"):
		pass	
	elif int(config_contents["nice"]) > -21 and int(config_contents["nice"] < 20):
			ac_settings["nice"]=config_contents["nice"]
	else:
		raise ValueError("FAILURE: Nice must be a number between -20 and 19. Setting was: " + config_contents["nice"])

	# Configure concurrency - If set.

	if config_contents.has_key("concurrency"):
			ac_settings["concurrency"]=true_false.get(config_contents["concurrency"], False)

	# All done.

	return ac_settings

###############################
# ac_stateinit - configures the state DIR. Extracted from the original configuration utility because there's
# sometimes a need to call this from other functions if the statedir is wiped or fails.
###############################


def ac_stateinit():

	# Let's build a list of files and directories that must exist.

	files = ( 
		"queue", 
		"lastcleanup",
		"cachesize", 
		)

	# If we have to create sub directories, make sure to create them IN ORDER.

	dirs = (
		"download", 
		"download/active",
		"download/locks",
		)

	for each in dirs:

		each=ac_settings["state_dir"]+each

		try:
			if not os.access(each,os.R_OK + os.W_OK) or not os.path.isdir(each):
				os.mkdir(each)	

		except OSError:
			# If we can't make the download DIR (something that can't ever happen, because at this point, the state_dir should exist and be writable,
			# who knows, maybe it got replaced with a file or something?
			ac_log.critical("FAILURE: Could not set up state directory: "+each)
			raise OSError("FAILURE: Could not set up state directory: "+each)


	for each in files:

		each=ac_settings["state_dir"]+each

		try:
			if not os.access(each,os.R_OK + os.W_OK) or not os.path.isfile(each):
				file(each,'a').close()
		except IOError:
			ac_log.critical("FAILURE: Could not read/write/create the state file: "+each)
			raise IOError("FAILURE: Could not read/write/create the state file: "+each)


	return

###############################
# ac_diskfree - lambda function, returning free disk space of path in percent.\
# May want to obsolete this function in exchange for determining the actual size of the cache to give better control.
# However, this function is very, -very- fast.
###############################

ac_df = lambda path: int(float(os.statvfs(path).f_bfree) / float(os.statvfs(path).f_blocks) * 100)

###############################
# ac_du - A terrible, complex lambda function, returning disk usage in bytes of a given path.
#
# A quick rundown of how this works:
# -Open a subprocess for du, summary in megabytes (-sm), path.
# -Put stdout and stderr into the communicate object.
# -Select the stdout, which is 0
# -Split the stdout by \t.
# -Return the first chunk of the split output.
#
# Takes a long time to execute because it's VERY I/O heavy, so it should be used SPARINGLY.
################################

ac_du = lambda path: int(subprocess.Popen((ac_settings["du_exec"],"-sb",path),stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()[0].split("\t")[0])

################################
# ac_urltohash - Another simple lambda function that turns a URL into an MD5 hash.
# This is what will be stored in the cache_dir. Standalone so that the generator can be easily modified.
################################

ac_urltohash = lambda url: hashlib.md5(urlparse.urlparse(url).path).hexdigest()

###############################
# ac_urltofile - A short, simple, stupid lambda which returns a filename for a given url.
# Similar to urltohash, this is so that we can change the way filenames are generated throughout the whole
# program easily.
##############################

ac_urltofile = lambda url: str(url).split('/')[-1]

# Stuff this library has to do.

ac_settings=ac_configure("config","")
ac_log=ac_settings["logger"]
ac_stateinit()
